/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2009 - 2012 Luca Mingardi.
 *
 * This file is part of jeeObserver.
 *
 * JeeObserver is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * JeeObserver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package jeeobserver;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import jeeobserver.logger.LoggerFormatter;
import jeeobserver.server.JeeObserverServerContext;
import jeeobserver.server.JeeObserverServerException;

/**
 * The Class JeeObserverContext.
 *
 * <p>
 * Main Class of jeeObserver core.<br/>
 * Use
 * <code>createInstance</code> method to start monitoring context and
 * <code>close</close> to stop monitoring context.<br/>
 * Use JeeObserverContextProperties class as parameter.<br/><br/>
 * Usually started by JeeObserverListener registered into monitored applications could also started and stopped manually.
 * </p>
 *
 * @author Luca Mingardi
 * @version 4.0
 */
public class JeeObserverContext {

	 /** The Constant VERSION. */
	 public static final int[] VERSION = {4, 0, 0};

	 /** The Constant CONTEXT_PARAMETER. */
	 public static final String CONTEXT_PARAMETER = "JO_CONTEXT";

	 /** The Constant LOGGER_LEVEL_PARAMETER. */
	 public static final String LOGGER_LEVEL_PARAMETER = "JO_LOGGER_LEVEL";

	 /** The Constant SERVER_HOST_PARAMETER. */
	 public static final String SERVER_HOST_PARAMETER = "JO_SERVER_HOST";

	 /** The Constant SERVER_PORT_PARAMETER. */
	 public static final String SERVER_PORT_PARAMETER = "JO_SERVER_PORT";

	 /** The Constant SOCKET_BUFFER_SIZE_PARAMETER. */
	 public static final String BUFFER_SIZE_PARAMETER = "JO_BUFFER_SIZE";

	 /** The Constant SOCKET_INTERVAL_PARAMETER. */
	 public static final String TIMER_INTERVAL_PARAMETER = "JO_TIMER_INTERVAL";

	 /** The Constant DEFAULT_context. */
	 public static final String DEFAULT_CONTEXT = "DEFAULT_CONTEXT";

	 /** The Constant DEFAULT_SERVER_HOST. */
	 public static final String DEFAULT_SERVER_HOST = "localhost";

	 /** The Constant DEFAULT_SERVER_PORT. */
	 public static final int DEFAULT_SERVER_PORT = 5688;

	 /** The Constant DEFAULT_BUFFER_SIZE. */
	 public static final int DEFAULT_BUFFER_SIZE = 100000;

	 /** The Constant DEFAULT_SOCKET_INTERVAL. */
	 public static final int DEFAULT_TIMER_INTERVAL = 5000;

	 /** The Constant DEFAULT_LOGGER_LEVEL. */
	 public static final Level DEFAULT_LOGGER_LEVEL = Level.INFO;

	 /** The Constant SOCKET_TASK_NAME. */
	 public static final String TIMER_TASK_NAME = "jeeobserverTimerTask";

	 /** The Constant DEFAULT_CONFIG_FILE. */
	 public static final String CONFIG_FILE = "jeeobserver";

	 /** The instance. */
	 private static JeeObserverContext instance;

	 /** The enabled. */
	 private boolean enabled = false;

	 /** The context name. */
	 private final String context;

	 /** The server host. */
	 private final String serverHost;

	 /** The server port. */
	 private final int serverPort;

	 /** The socket buffer size. */
	 private final int bufferSize;

	 /** The socket interval. */
	 private final int timerInterval;

	 /** The Constant logger. */
	 public static final Logger logger = JeeObserverContext.createLogger(JeeObserverContext.DEFAULT_LOGGER_LEVEL);

	 /** The socket timer. */
	 private final Timer timer;

	 private final ObserverBulk<Observer> buffer;

	 public JeeObserverContext(String context, String serverHost, int serverPort, int bufferSize, int timerInterval) {
			this.context = context;
			this.serverHost = serverHost;
			this.serverPort = serverPort;
			this.bufferSize = bufferSize;
			this.timerInterval = timerInterval;

			//Initialize buffer
			this.buffer = new ObserverBulk(this.bufferSize);

			//Start timer task
			this.timer = new Timer(JeeObserverContext.TIMER_TASK_NAME, true);

			this.enabled = true;

			JeeObserverContext.logger.log(Level.INFO, "JeeObserver context instance created.");
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.LOGGER_LEVEL_PARAMETER, JeeObserverContext.logger.getLevel().getName()});
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.CONTEXT_PARAMETER, this.context});
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.SERVER_HOST_PARAMETER, String.valueOf(this.serverHost)});
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.SERVER_PORT_PARAMETER, String.valueOf(this.serverPort)});
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.BUFFER_SIZE_PARAMETER, String.valueOf(this.bufferSize)});
			JeeObserverContext.logger.log(Level.FINE, "JeeObserver parameter: {0} = {1}", new Object[]{JeeObserverContext.TIMER_INTERVAL_PARAMETER, String.valueOf(this.timerInterval)});
	 }

	 /**
	  * Gets the single instance of JeeObserverContext.
	  *
	  * @return single instance of JeeObserverContext
	  */
	 public synchronized static JeeObserverContext getInstance() {
			if (JeeObserverContext.instance == null) {
				 JeeObserverContext.logger.log(Level.WARNING, "JeeObserver server context not yet created.");

				 //JeeObserverContext.createInstance();
			}

			return JeeObserverContext.instance;
	 }

	 /**
	  * Creates the instance.
	  *
	  * @return the jee observer server context
	  *
	  */
	 public static JeeObserverContext createInstance() {

			final Map<String, String> parameters = new HashMap<String, String>();

			// 1 - Load System variables
			for (String key : System.getenv().keySet()) {
				 String value = System.getenv().get(key);
				 if (value != null) {
						parameters.put(key, value.trim());
				 }
			}

			// 2 - Load resource Boundle
			try {

				 //Try to load resource boundle from all classLoaders using parent linkage
				 ResourceBundle resourceBundle = null;

				 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
				 do {
						try {
							 resourceBundle = ResourceBundle.getBundle(CONFIG_FILE, Locale.getDefault(), classLoader);
						} catch (MissingResourceException e) {
							 //If resource was not found, try load it from a parent classloader
							 classLoader = classLoader.getParent();
							 if (classLoader == null) {
									//If there is not parent classloader, bail out with a rethrow
									throw e;
							 }
						}
				 } while (resourceBundle == null);

				 Enumeration<String> keys = resourceBundle.getKeys();
				 while (keys.hasMoreElements()) {
						String key = keys.nextElement();
						String value = resourceBundle.getString(key);
						if (value != null) {
							 parameters.put(key, value.trim());
						}
				 }

			} catch (final MissingResourceException e) {
				 JeeObserverServerContext.logger.log(Level.FINEST, e.getMessage(), e);
			}

			return JeeObserverContext.createInstance(parameters);
	 }

	 /**
	  * Creates the instance.
	  *
	  * @param parameters
	  *
	  * @return the jee observer context
	  */
	 public synchronized static JeeObserverContext createInstance(Map<String, String> parameters) {

			if (parameters.containsKey(JeeObserverContext.LOGGER_LEVEL_PARAMETER)) {
				 try {
						JeeObserverServerContext.logger.setLevel(Level.parse(parameters.get(JeeObserverContext.LOGGER_LEVEL_PARAMETER)));
				 } catch (final Exception e) {
						JeeObserverContext.logger.log(Level.SEVERE, "Parameter " + JeeObserverContext.LOGGER_LEVEL_PARAMETER + " = {0} is not a valid Level. (Available values: OFF | SEVERE | WARNING | INFO | FINE | FINER | FINEST | ALL)", parameters.get(JeeObserverContext.LOGGER_LEVEL_PARAMETER));
						JeeObserverContext.logger.log(Level.FINEST, e.getMessage(), e);
				 }
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.LOGGER_LEVEL_PARAMETER + " not found.");
			}

			String context = JeeObserverContext.DEFAULT_CONTEXT;
			if (parameters.containsKey(JeeObserverContext.CONTEXT_PARAMETER)) {
				 context = parameters.get(JeeObserverContext.CONTEXT_PARAMETER);
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.CONTEXT_PARAMETER + " not found.");
				 //Try to set current ip as context
				 try {
						context = InetAddress.getLocalHost().getHostAddress();
				 } catch (final UnknownHostException e) {
						JeeObserverContext.logger.log(Level.FINEST, e.getMessage(), e);
				 }
			}

			String serverHost = JeeObserverContext.DEFAULT_SERVER_HOST;
			if (parameters.containsKey(JeeObserverContext.SERVER_HOST_PARAMETER)) {
				 serverHost = parameters.get(JeeObserverContext.SERVER_HOST_PARAMETER);
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.SERVER_HOST_PARAMETER + " not found.");
			}

			int serverPort = JeeObserverContext.DEFAULT_SERVER_PORT;
			if (parameters.containsKey(JeeObserverContext.SERVER_PORT_PARAMETER)) {
				 try {
						serverPort = Integer.parseInt(parameters.get(JeeObserverContext.SERVER_PORT_PARAMETER));
				 } catch (final NumberFormatException e) {
						JeeObserverContext.logger.log(Level.SEVERE, "Parameter " + JeeObserverContext.SERVER_PORT_PARAMETER + " = {0} is not a number.", parameters.get(JeeObserverContext.SERVER_PORT_PARAMETER));
						JeeObserverContext.logger.log(Level.FINEST, e.getMessage(), e);
				 }
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.SERVER_PORT_PARAMETER + " not found.");
			}



			int bufferSize = JeeObserverContext.DEFAULT_BUFFER_SIZE;
			if (parameters.containsKey(JeeObserverContext.BUFFER_SIZE_PARAMETER)) {
				 try {
						bufferSize = Integer.parseInt(parameters.get(JeeObserverContext.BUFFER_SIZE_PARAMETER));
				 } catch (final NumberFormatException e) {
						JeeObserverContext.logger.log(Level.SEVERE, "Parameter " + JeeObserverContext.BUFFER_SIZE_PARAMETER + " = {0} is not a number.", parameters.get(JeeObserverContext.BUFFER_SIZE_PARAMETER));
						JeeObserverContext.logger.log(Level.FINEST, e.getMessage(), e);
				 }
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.BUFFER_SIZE_PARAMETER + " not found.");
			}

			int timerInterval = JeeObserverContext.DEFAULT_TIMER_INTERVAL;
			if (parameters.containsKey(JeeObserverContext.TIMER_INTERVAL_PARAMETER)) {
				 try {
						timerInterval = Integer.parseInt(parameters.get(JeeObserverContext.TIMER_INTERVAL_PARAMETER));
				 } catch (final NumberFormatException e) {
						JeeObserverContext.logger.log(Level.SEVERE, "Parameter " + JeeObserverContext.TIMER_INTERVAL_PARAMETER + " = {0} is not a number.", parameters.get(JeeObserverContext.TIMER_INTERVAL_PARAMETER));
						JeeObserverContext.logger.log(Level.FINEST, e.getMessage(), e);
				 }
			} else {
				 JeeObserverContext.logger.log(Level.FINE, "Parameter " + JeeObserverContext.TIMER_INTERVAL_PARAMETER + " not found.");
			}


			return createInstance(context, serverHost, serverPort, bufferSize, timerInterval);
	 }

	 /**
	  * Creates the instance.
	  *
	  * @param context the context name
	  * @param serverHost the server host
	  * @param serverPort the server port
	  *
	  * @return the jee observer context
	  */
	 public synchronized static JeeObserverContext createInstance(String context, String serverHost, int serverPort) {
			return createInstance(context, serverHost, serverPort, JeeObserverContext.DEFAULT_BUFFER_SIZE, JeeObserverContext.DEFAULT_TIMER_INTERVAL);

	 }

	 /**
	  * Creates the instance.
	  *
	  * @param context the context name
	  * @param serverHost the server host
	  * @param serverPort the server port
	  *
	  * @param bufferSize
	  * @param timerInterval
	  *
	  * @return the jee observer context
	  */
	 public synchronized static JeeObserverContext createInstance(String context, String serverHost, int serverPort, int bufferSize, int timerInterval) {

			if (JeeObserverContext.instance == null) {

				 JeeObserverContext.instance = new JeeObserverContext(context, serverHost, serverPort, bufferSize, timerInterval);

				 JeeObserverContext.instance.timer.schedule(new ContextTimerTask(), 0, timerInterval);


			} else {

				 JeeObserverContext.logger.log(Level.SEVERE, "JeeObserver context instance not created. Instance already running.");
			}

			return JeeObserverContext.instance;

	 }

	 public synchronized void close() throws JeeObserverServerException {

			if (this.enabled) {
				 this.enabled = false;
				 this.timer.purge();
				 this.timer.cancel();

				 // Flush all cached data
				 this.flush();

				 JeeObserverContext.logger.log(Level.INFO, "JeeObserver context instance destroyed.");
			}

			JeeObserverContext.instance = null;
	 }

	 synchronized void addObserver(Observer observer) {
			if (this.isEnabled() && this.buffer.size() < this.bufferSize) {
				 this.buffer.add(observer);
			} else {
				 JeeObserverContext.logger.log(Level.WARNING, "JeeObserver buffer full. Observer ignored.");
			}
	 }

	 /**
	  * Send data.
	  */
	 synchronized void sendObserverBulk() {

			if (this.buffer == null || this.buffer.isEmpty()) {
				 return;
			}

			final DecimalFormat percentFormat = new DecimalFormat("#0.00%");


			while (!this.buffer.isEmpty()) {

				 	final double bufferUsage = (Long.valueOf(this.buffer.size()).doubleValue() / Long.valueOf(this.bufferSize).doubleValue());

				 ObserverBulk<Observer> bulk = new ObserverBulk(1000);

				 while (!this.buffer.isEmpty() && bulk.size() < 1000) {
						bulk.add(this.buffer.remove());
				 }

				 try {
						final Socket socket = new Socket(this.serverHost, this.serverPort);

						final ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
						final ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());

						long startTimeMillis = System.currentTimeMillis();

						objectOutputStream.writeObject(bulk);
						objectOutputStream.flush();

						long stopTimeMillis = System.currentTimeMillis();

						JeeObserverContext.logger.log(Level.FINE, "Observer bulk sent: Time: {0} ms. - Buffer: {1}. - Invocations: {2}", new Object[]{String.valueOf(stopTimeMillis - startTimeMillis), percentFormat.format(bufferUsage), String.valueOf(buffer.size())});

						objectInputStream.readObject();

						objectOutputStream.close();
						objectInputStream.close();

						socket.close();

				 } catch (final Exception e) {

						while (!bulk.isEmpty()) {
							 this.addObserver(bulk.remove());
						}

						JeeObserverContext.logger.log(Level.FINE, e.getMessage(), e);

						JeeObserverContext.logger.log(Level.INFO, "Problem sending data to jeeObserver server. Server may be down. Buffer usage:  {0}.", percentFormat.format(bufferUsage));

						//On error don't retry, wait next send.
						break;
				 }

				 if (this.buffer.size() < 1000) {
						//If bulk is not full wait next send.
						break;
				 }
			}
	 }

	 /**
	  * Flush.
	  */
	 public synchronized void flush() {
			JeeObserverContext.logger.log(Level.FINE, "Flush JeeObserver context buffer.");
			while (!this.buffer.isEmpty()) {
				 this.sendObserverBulk();
			}
	 }

	 public boolean isEnabled() {
			return this.enabled;
	 }

	 public String getContext() {
			return context;
	 }

	 /**
	  * Creates the logger.
	  *
	  * @param loggerLevel the logger level
	  *
	  * @return the logger
	  */
	 private static Logger createLogger(Level loggerLevel) {

			final Logger newLogger = Logger.getLogger("jeeobserver");

			newLogger.setUseParentHandlers(false);

			newLogger.setLevel(loggerLevel);

			final Handler handlerArray[] = newLogger.getHandlers();

			for (final Handler element : handlerArray) {
				 newLogger.removeHandler(element);
			}

			final Handler handler = new ConsoleHandler();
			handler.setLevel(Level.ALL);
			handler.setFormatter(new LoggerFormatter());

			newLogger.addHandler(handler);

			return newLogger;
	 }

	 /**
	  * The Class SocketTimerTask.
	  *
	  * @author Luca Mingardi
	  * @version 4.0
	  */
	 private static class ContextTimerTask extends TimerTask {

			private final JvmObserver jvmObserver;

			private final HardDiskObserver hardDiskObserver;

			ContextTimerTask() {


				 //Check if hard disk monitoring is supported
				 boolean hardDiskMonitoringEnabled = false;

				 final Method fileMethods[] = File.class.getDeclaredMethods();
				 for (final Method fileMethod : fileMethods) {
						if (fileMethod.getName().equals("getTotalSpace")) {
							 hardDiskMonitoringEnabled = true;
						}
				 }
				 if (!hardDiskMonitoringEnabled) {
						JeeObserverContext.logger.log(Level.WARNING, "Can''t start disk monitoring because current SDK does not support it. (Required SDK >= 1.6)");
				 }

				 if (hardDiskMonitoringEnabled) {
						this.hardDiskObserver = new HardDiskObserver();
				 } else {
						this.hardDiskObserver = null;
				 }

				 this.jvmObserver = new JvmObserver();
			}

			@Override
			public synchronized void run() {


				 try {
						//Jvm
						if (this.jvmObserver.isRunning()) {
							 this.jvmObserver.stop();
						}
						this.jvmObserver.start();


						//Hard Disk
						if (this.hardDiskObserver != null) {
							 this.hardDiskObserver.invoke();
						}
				 } catch (final Exception e) {
						JeeObserverContext.logger.log(Level.SEVERE, e.getMessage(), e);
				 }


				 // Send cached data
				 if ((JeeObserverContext.getInstance() != null) && JeeObserverContext.getInstance().isEnabled()) {
						JeeObserverContext.getInstance().sendObserverBulk();
				 }
			}
	 }
}
